﻿using System;
using System.Collections.Specialized;
using System.Threading;
using System.Threading.Tasks;
using FLib;
using LibraryGuard.Interfaces;
using LibraryGuard.Interfaces.Configuration;

namespace LibraryGuard.Modes
{
	/// <summary>
	/// Base mode for some of them.
	/// </summary>
	public abstract class BaseMode
		: IMode
	{
		#region Logger
		protected static NLog.Logger Logger = null;
		#endregion

		#region Config
		private static readonly TimeSpan SingleDelay = TimeSpan.FromSeconds(0.5);
		private static readonly TimeSpan DelayAmount = TimeSpan.FromSeconds(5);
		#endregion

		#region Fields
		private readonly IConfiguration Config = null;
		private readonly IGuard[] Guards = null;
		protected readonly IActionList ActionList = null;

		private CancellationTokenSource CTS = null;
		private volatile int TaskCount = 0;
		#endregion

		#region Constructor
		public BaseMode(IConfiguration config, IGuard[] guards, IActionList actionList)
		{
			this.Config = config;
			this.Guards = guards;
			this.ActionList = actionList;

			this.State = ModeState.Stopped;
		}
		#endregion

		#region IMode Members
		/// <inheritdoc />
		public ModeState State { get; private set; }

		/// <inheritdoc />
		public void Start()
		{
			Validate.Debug(() => this.State, v => v.Equal(ModeState.Stopped));
			Logger.Debug("Starting");

			this.ActionList.CollectionChanged += this.ActionAdded;
			this.CTS = new CancellationTokenSource();
			this.TaskCount = 0;
			this.RunTask(this.RunAllGuards);

			Logger.Info("Started");
			this.State = ModeState.Running;
		}

		/// <inheritdoc />
		public void Stop(bool forceStop)
		{
			Validate.Debug(() => this.State, v => v.That(s => s == ModeState.Running || s == ModeState.Paused));
			Logger.Debug("Stopping");

			if (this.State == ModeState.Running)
			{
				if (forceStop)
				{
					this.ActionList.CollectionChanged -= this.ActionAdded;
					this.CTS.Cancel();
				}
				this.WaitForAllTasks();

				this.ActionList.Clear();
				this.ActionList.CollectionChanged -= this.ActionAdded;
				this.CTS.Dispose();
				this.CTS = null;
			}
			else
			{
				this.ActionList.Clear();
			}

			this.State = ModeState.Stopped;
			Logger.Info("Stopped");
		}

		/// <inheritdoc />
		public void Pause()
		{
			Validate.Debug(() => this.State, v => v.Equal(ModeState.Running));
			Logger.Debug("Pausing");

			this.ActionList.CollectionChanged -= this.ActionAdded;
			this.CTS.Cancel();
			this.WaitForAllTasks();
			this.CTS.Dispose();
			this.CTS = null;

			this.State = ModeState.Paused;
			Logger.Info("Paused");
		}

		/// <inheritdoc />
		public void Resume()
		{
			Validate.Debug(() => this.State, v => v.Equal(ModeState.Paused));
			Logger.Debug("Resuming");

			this.CTS = new CancellationTokenSource();
			this.TaskCount = 0;
			this.ActionList.CollectionChanged += this.ActionAdded;

			//Spawning all remaining actions
			//Lock prevents from any changes in ActionList.
			lock (this.ActionList)
			{
				for (int i = 0; i < this.ActionList.Count; i++)
				{
					var action = this.ActionList[i];
					this.RunTask(() => this.Execute(action));
				}
			}

			this.State = ModeState.Running;
			Logger.Info("Resumed");
		}
		#endregion

		#region IDisposable Members
		/// <inheritdoc />
		public void Dispose()
		{
			if (this.CTS != null)
				this.CTS.Dispose();
		}
		#endregion

		#region Protected methods to override
		/// <summary>
		/// Executes action.
		/// </summary>
		/// <param name="action"></param>
		protected abstract void ExecuteAction(IActionInfo action);
		#endregion

		#region Auxiliary Methods
		/// <summary>
		/// Runs delayed task.
		/// </summary>
		/// <param name="action"></param>
		protected void RunDelayedTask(Action action)
		{
			var token = this.CTS.Token;
			if (token.IsCancellationRequested)
				return;

			this.RunTask(() =>
			{
				var start = DateTime.Now;
				while (!token.IsCancellationRequested && (DateTime.Now - start) < DelayAmount)
					Thread.Sleep(SingleDelay);
				if (!this.CTS.Token.IsCancellationRequested)
					action();
			});
		}
		#endregion

		#region Private Methods
		/// <summary>
		/// Handles <i>Add</i> event.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void ActionAdded(object sender, NotifyCollectionChangedEventArgs e)
		{
			if (e.Action == NotifyCollectionChangedAction.Add)
			{
				this.RunTask(() => this.Execute((IActionInfo)e.NewItems[0]));
			}
		}

		/// <summary>
		/// Runs all guards.
		/// </summary>
		private void RunAllGuards()
		{
			var token = this.CTS.Token;
			for (int i = 0; i < this.Guards.Length && !token.IsCancellationRequested; i++)
			{
				RunGuard(this.Guards[i]);
			}
			//We don't need this
			//this.CTS.Token.ThrowIfCancellationRequested();
		}

		/// <summary>
		/// Runs single guard.
		/// Extracted from RunAllGuards to simplify delayed execution.
		/// </summary>
		/// <param name="guard"></param>
		private void RunGuard(IGuard guard)
		{
			var type = guard.GetType().Name;
			try
			{
				Logger.Debug("Running " + type);
				guard.Run();
				Logger.Info("{0} run successfully", type);
			}
			catch (Exception ex)
			{
				Logger.ErrorException("Cannot run {0} guard, delayed".FormatWith(type), ex);
				this.RunDelayedTask(() => this.RunGuard(guard));
			}
		}

		/// <summary>
		/// Executes action.
		/// </summary>
		/// <param name="action"></param>
		private void Execute(IActionInfo action)
		{
			var token = this.CTS.Token;

			//We don't need to throw exceptions
			if (token.IsCancellationRequested)
				return;

			lock (this.ActionList)
			{
				if (token.IsCancellationRequested)
					return;

				if (!this.ActionList.Remove(action))
					return;
				//CONSIDER: From this point we treat action as executed
			}

			this.ExecuteAction(action);
		}

		/// <summary>
		/// Runs task that will be added to <see cref="Tasks"/> list.
		/// </summary>
		/// <param name="action"></param>
		private void RunTask(Action action)
		{
			//CONSIDER: Is interlocked necessary?
			var t = new Task(action, this.CTS.Token);
			t.ContinueWith(_ => --this.TaskCount);
			++this.TaskCount;
			t.Start();
		}

		/// <summary>
		/// Waits for all tasks to complete.
		/// Waits for <see cref="TaskCount"/> to reach zero.
		/// </summary>
		private void WaitForAllTasks()
		{
			SpinWait spinWait = new SpinWait();
			while (this.TaskCount != 0)
				spinWait.SpinOnce();
		}
		#endregion
	}
}
